// Copyright 2010 Alexandre Tristan St-Cyr <tristan.stcyr@gmail.com>

package com.codeplex.loadjava;

import static org.mockito.Matchers.any;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;

import java.io.Serializable;

import org.junit.Test;
import org.mockito.InOrder;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;

import test.packge.name.ClassWithInnerClass;
import test.packge.name.ClassWithInterfaces;
import test.packge.name.SimpleClass;

import com.codeplex.loadjava.BufferedJavaEntityAdapter;
import com.codeplex.loadjava.IJavaModelVisitor;
import com.codeplex.loadjava.JavaModelReader;
import com.codeplex.loadjava.model.ClassDefinition;
import com.codeplex.loadjava.model.Field;
import com.codeplex.loadjava.model.FieldDefinition;
import com.codeplex.loadjava.model.FieldReference;
import com.codeplex.loadjava.model.InterfaceImplementation;
import com.codeplex.loadjava.model.JavaClass;
import com.codeplex.loadjava.model.Method;
import com.codeplex.loadjava.model.MethodDefinition;
import com.codeplex.loadjava.model.MethodInvocation;
import com.codeplex.loadjava.model.TypeReference;


public class JavaMethodReaderTest {
	
	@Test
	public void testSimpleClass()
	{
		JavaClass objectClass = new JavaClass(Type.getInternalName(Object.class));
		ClassDefinition classDefinition = new ClassDefinition(
				Type.getInternalName(SimpleClass.class), 33, null, objectClass, "");
		
		IJavaModelVisitor mockVisitor = getMockVisitor(
				"inputbin/test/packge/name/SimpleClass.class");
		
		verify(mockVisitor).visitClass(objectClass);
		verify(mockVisitor).visitClass(classDefinition);
		
		verify(mockVisitor).visitClassDefinition(classDefinition);
		
		Method objectCtor = new Method(objectClass, "<init>", "()V");
		Method simpleClassCtor = new Method(classDefinition, "<init>", "()V");

		verify(mockVisitor).visitMethod(simpleClassCtor);
		verify(mockVisitor).visitMethod(objectCtor);		
		verify(mockVisitor).visitType(new TypeReference("V"));
		
		verify(mockVisitor, times(2)).visitMethod(any(Method.class));
		verify(mockVisitor, times(0)).visitField(any(Field.class));
		verify(mockVisitor, times(2)).visitClass(any(JavaClass.class));
	}
	
	@Test
	public void testFields()
	{	
		IJavaModelVisitor mockVisitor = getMockVisitor("inputbin/test/packge/name/ClassWithFields.class");
		
		JavaClass javaClass = new JavaClass("test/packge/name/ClassWithFields");
		FieldDefinition publicIntField = new FieldDefinition(javaClass, "publicInt", 
				new TypeReference("I"), Opcodes.ACC_PUBLIC, null);
		FieldDefinition privateLong = new FieldDefinition(javaClass, "privateLongs", 
				new TypeReference("[[J"), Opcodes.ACC_PRIVATE, null);
		FieldDefinition defaultShorts = new FieldDefinition(javaClass, "defaultShorts", 
				new TypeReference("Ljava/util/Stack;"), 0, "Ljava/util/Stack<Ljava/lang/Short;>;");
		FieldDefinition protectedStrings = new FieldDefinition(javaClass, "protectedStrings", 
				new TypeReference("Ljava/util/List;"), Opcodes.ACC_PROTECTED, 
				"Ljava/util/List<Ljava/lang/String;>;");
		
		FieldDefinition[] fields = new FieldDefinition[] {
				publicIntField, 
				privateLong, 
				defaultShorts, 
				protectedStrings
		};
		
		for(FieldDefinition field : fields) {
			InOrder inOrder = inOrder(mockVisitor);
			inOrder.verify(mockVisitor).visitField(field);
			inOrder.verify(mockVisitor).visitFieldDefinition(field);
		}
		
		verify(mockVisitor, times(fields.length)).visitField(any(Field.class));
		verify(mockVisitor, times(fields.length+1)).visitType(any(TypeReference.class));
		verify(mockVisitor, times(4)).visitClass(any(JavaClass.class));
	}
	
	@Test
	public void testMethods()
	{	
		IJavaModelVisitor mockVisitor = getMockVisitor("inputbin/test/packge/name/ClassWithMethods.class");
		final String classInternalName = "test/packge/name/ClassWithMethods";
		
		JavaClass javaClass = new JavaClass(classInternalName);
		MethodDefinition[] methodDefs = {
			new MethodDefinition(javaClass, "<init>", "()V", Opcodes.ACC_PUBLIC, null),
			new MethodDefinition(javaClass, "getV", "(I)V", Opcodes.ACC_PUBLIC, null),
			new MethodDefinition(javaClass, "getI", "()I", Opcodes.ACC_PROTECTED, null),
			new MethodDefinition(javaClass, "getO", "()Ljava/lang/Object;", Opcodes.ACC_PRIVATE, null),
			new MethodDefinition(javaClass, "getObjects", "()[Ljava/lang/Object;", 0, null)
		};
		
		for(MethodDefinition methodDef : methodDefs) {
			InOrder inOrder = inOrder(mockVisitor);
			inOrder.verify(mockVisitor).visitMethod(methodDef);
			inOrder.verify(mockVisitor).visitMethodDefinition(methodDef);
		}
		
		verify(mockVisitor).visitMethod(
				new Method(new JavaClass("java/lang/Object"), "<init>", "()V"));
		
		verify(mockVisitor, times(methodDefs.length+1)).visitMethod(any(Method.class));
		verify(mockVisitor, times(methodDefs.length)).visitMethodDefinition(any(MethodDefinition.class));
		
		verify(mockVisitor).visitType(new TypeReference("V"));
		verify(mockVisitor).visitType(new TypeReference("I"));
		verify(mockVisitor).visitType(new TypeReference("Ljava/lang/Object;"));
	}
	
	@Test
	public void testFieldReferences()
	{
		final JavaClass javaClass = new JavaClass("test/packge/name/FieldReferences$InnerClass");
		IJavaModelVisitor mockVisitor = getMockVisitor("inputbin/"+javaClass.getInternalName()+".class");
		
		Method referencingOtherClassMethod = new Method(javaClass, "referencingOtherClass", "()I");
		
		// References to field i
		TypeReference intType = new TypeReference("I");
		Field iField = new Field(javaClass, "i", intType);
		InOrder inOrder = inOrder(mockVisitor);
		inOrder.verify(mockVisitor, times(1)).visitType(intType);
		inOrder.verify(mockVisitor, times(1)).visitField(iField);
		inOrder.verify(mockVisitor, times(1)).visitFieldReference(new FieldReference(
				iField, new Method(javaClass, "referencingSameClass", "()I")));
		
		// References to field j
		TypeReference floatType = new TypeReference("F");
		Field jField = new Field(new JavaClass("test/packge/name/FieldReferences"), "j", floatType);
		inOrder = inOrder(mockVisitor);
		inOrder.verify(mockVisitor, times(1)).visitType(floatType);
		inOrder.verify(mockVisitor, times(1)).visitField(jField);
		inOrder.verify(mockVisitor, times(1)).visitFieldReference(new FieldReference(
				jField, referencingOtherClassMethod));
		
		// References to field this$0
		TypeReference classType = new TypeReference("Ltest/packge/name/FieldReferences;");
		Field thisField = new Field(javaClass, "this$0", classType);
		inOrder = inOrder(mockVisitor);
		inOrder.verify(mockVisitor, times(1)).visitType(classType);
		inOrder.verify(mockVisitor, times(1)).visitField(thisField);
		inOrder.verify(mockVisitor, times(1)).visitFieldReference(new FieldReference(
				thisField, new Method(javaClass, "<init>", "(Ltest/packge/name/FieldReferences;)V")));
	}
	
	@Test
	public void testMethodInvocations()
	{
		JavaClass innerClass = new JavaClass("test/packge/name/MethodInvocations$InnerClass");
		IJavaModelVisitor mockVisitor = getMockVisitor("inputbin/"+innerClass.getInternalName()+".class");
		
		// Verify the call within the same class
		Method method = new Method(innerClass, "method", "()V");
		Method sameClassMethod = new Method(innerClass, "sameClassInvocation", "()V");
		MethodInvocation sameClassInvoc = new MethodInvocation(sameClassMethod, method);
		InOrder inOrder = inOrder(mockVisitor);
		inOrder.verify(mockVisitor, times(1)).visitMethod(method);
		inOrder.verify(mockVisitor, times(1)).visitMethod(sameClassMethod);
		inOrder.verify(mockVisitor, times(1)).visitMethodInvocation(sameClassInvoc);
		
		// Veriy the call to another class
		JavaClass outerClass = new JavaClass("test/packge/name/MethodInvocations");
		Method outerMethod = new Method(outerClass, "outerMethod", "()V");
		Method otherClassMethod = new Method(innerClass, "otherClassInvocation", "()V");
		MethodInvocation otherClassInvoc = new MethodInvocation(otherClassMethod, outerMethod);
		inOrder = inOrder(mockVisitor);
		inOrder.verify(mockVisitor).visitClass(outerClass);
		inOrder.verify(mockVisitor).visitMethod(outerMethod);
		inOrder.verify(mockVisitor).visitMethodInvocation(otherClassInvoc);
	}
	
	@Test
	public void testJar()
	{
		IJavaModelVisitor mockVisitor = getMockVisitor("testjars/testjar.jar");
		JavaClass testJarClass = new JavaClass("test/packge/name/TestJar");
		verify(mockVisitor, times(1)).visitClass(testJarClass);
	}
	
	@Test
	public void testInterfaceImplementations()
	{
		IJavaModelVisitor mockVisitor = getMockVisitor(
				"inputbin/test/packge/name/ClassWithInterfaces.class");
		
		ClassDefinition implementation = new ClassDefinition(
				Type.getInternalName(ClassWithInterfaces.class), 33, 
				"Ljava/lang/Object;Ljava/io/Serializable;Ljava/lang/Iterable<Ljava/lang/String;>;", 
				getJavaClass(Object.class), "");
		
		// Verify Serializable implementation
		JavaClass serializableClass = getJavaClass(Serializable.class);
		InterfaceImplementation serializableImpl = 
			new InterfaceImplementation(implementation, serializableClass);
		InOrder inOrder = inOrder(mockVisitor);
		inOrder.verify(mockVisitor).visitClassDefinition(implementation);
		inOrder.verify(mockVisitor).visitClass(serializableClass);
		inOrder.verify(mockVisitor).visitInterfaceImplementations(serializableImpl);
		
		// Verify Iterable implementation
		JavaClass iterableClass = getJavaClass(Iterable.class);
		InterfaceImplementation iterableImpl = 
			new InterfaceImplementation(implementation, iterableClass);
		inOrder = inOrder(mockVisitor);
		inOrder.verify(mockVisitor).visitClass(iterableClass);
		inOrder.verify(mockVisitor).visitInterfaceImplementations(iterableImpl);
	}
	
	private JavaClass getJavaClass(Class<?> clazz) {
		return new JavaClass(Type.getInternalName(clazz));
	}
	
	@Test
	public void testOuterClass()
	{
		String innerClassInternalName = Type.getInternalName(ClassWithInnerClass.InnerClass.class);
		IJavaModelVisitor mockVisitor = getMockVisitor(
			"inputbin/"+innerClassInternalName+".class");
		ClassDefinition classDefinition = new ClassDefinition(innerClassInternalName, 
				33, null, getJavaClass(Object.class), "");
		JavaClass outerClass = getJavaClass(ClassWithInnerClass.class);
		classDefinition.setOuterClass(outerClass);
		
		InOrder inOrder = inOrder(mockVisitor);
		inOrder.verify(mockVisitor).visitClass(outerClass);
		inOrder.verify(mockVisitor).visitClassDefinition(classDefinition);
	}

	private IJavaModelVisitor getMockVisitor(String classfilepath) {
		JavaModelReader reader = new JavaModelReader(classfilepath);
		IJavaModelVisitor mockVisitor = mock(IJavaModelVisitor.class);
		reader.accept(new BufferedJavaEntityAdapter(mockVisitor));
		return mockVisitor;
	}
}
